Compare commits

..

7 Commits

Author SHA1 Message Date
Egor Savkin 84562f276a Add horizontal items into the cart
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-11-06 17:18:23 +08:00
Egor Savkin bf17a24704 Make dialog popup rendered by bootstrap overlay library
This makes it more dynamic and eases proper placement in case of vertical layout

Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-11-06 16:55:49 +08:00
Egor Savkin c9ddc2513b Add AFWS item to the shop and update bundle
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-11-06 16:55:49 +08:00
Egor Savkin e7f187c6c7 Fix exception on thermostat2ch and build the bundle
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-11-06 16:55:49 +08:00
Egor Savkin 2dd5d2db1a Update cards
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-11-06 16:55:49 +08:00
Egor Savkin 729e4ea6fc Add possibility for crateless options
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-11-06 16:55:48 +08:00
Egor Savkin f82f9334fd Automatically open spare cards for crateless items
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-11-06 16:55:48 +08:00
23 changed files with 646 additions and 225 deletions

View File

@ -227,12 +227,6 @@ We welcome inquiries from research groups of all sizes.<br>[See what has been fu
<a href="https://github.com/vuthalab/artiq" target="_blank" rel="noopener noreferrer">Repository</a> <a href="https://github.com/vuthalab/artiq" target="_blank" rel="noopener noreferrer">Repository</a>
{% end %} {% end %}
{% layout_card(title="OffsetStabilizer", sameheight=120) %}
<small>Stabilizer firmware for laser frequency offset stabilization</small>
<a href="https://github.com/PhBrb/OffsetStabilizer" target="_blank" rel="noopener noreferrer">Repository</a>
{% end %}
</div> </div>
{% layout_div(css="col-12 text-center") %} {% layout_div(css="col-12 text-center") %}

View File

@ -117,10 +117,20 @@ button {
margin-bottom: 20px; margin-bottom: 20px;
} }
button {
img {
cursor: pointer;
}
}
img { img {
height: 400px; height: 400px;
align-self: center; align-self: center;
border: 0; border: 0;
cursor: grab;
&.default-icon {
height: 144px;
}
} }
h3 { h3 {
@ -485,6 +495,45 @@ button {
padding: 5px 5px 12px; padding: 5px 5px 12px;
position: relative; position: relative;
&.horizontal {
flex-direction: column;
min-height: 10px;
overflow-y: auto;
overflow-x: hidden;
width: 100%;
> div {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-content: center;
align-items: center;
width: 100%;
max-width: 100%;
padding: 5px 3px;
}
.progress-container {
max-width: 40px;
padding: 0 10px;
justify-self: start;
}
.product-name {
justify-self: start;
padding: 0 10px;
max-width: 100%;
width: 100%;
font-size: 1rem;
}
.removeHorizontal {
justify-self: end;
padding: 0 10px;
img {
width: 20px;
height: 20px;
}
}
}
> div { > div {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -571,67 +620,6 @@ button {
} }
} }
.overlayVariant {
top: 24px;
width: 140px;
min-height: 40px;
max-height: 320px;
overflow-y: scroll;
position: absolute;
display: flex;
align-items: start;
text-align: left;
background-color: white;
color: black;
flex-direction: column;
cursor: pointer;
padding: 0.2rem 0;
p {
font-size: .65rem;
margin: 0;
}
div {
margin: 0.1rem 0.2rem;
font-size: 0.75rem;
input {
padding: 0;
font-size: 0.75rem;
line-height: 1.1;
}
label {
margin-bottom: 0.1rem;
}
.options-icon {
display: inline;
height: .875rem;
margin-right: 0.2rem;
margin-left: 0.2rem;
}
}
.form-check {
min-height: 1rem;
}
}
}
.overlay-smallcard {
left: -38.5px; // (card width (63) - overlay width (140)) / 2
&.overlay-first {
left: 0;
}
&.overlay-last {
left: -67px;
}
}
.overlay-bigcard {
left: -7px; // (card width (126) - overlay width (140)) / 2
} }
.hovered { .hovered {
@ -778,3 +766,49 @@ button {
} }
} }
.overlayVariant {
width: 140px;
min-height: 40px;
max-height: 320px;
overflow-y: scroll;
position: absolute;
display: flex;
align-items: start;
text-align: left;
background-color: white;
color: black;
flex-direction: column;
cursor: pointer;
padding: 0.2rem 0;
p {
font-size: .65rem;
margin: 0;
}
div {
margin: 0.1rem 0.2rem;
font-size: 0.75rem;
input {
padding: 0;
font-size: 0.75rem;
line-height: 1.1;
}
label {
margin-bottom: 0.1rem;
}
.options-icon {
display: inline;
height: .875rem;
margin-right: 0.2rem;
margin-left: 0.2rem;
}
}
.form-check {
min-height: 1rem;
}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -0,0 +1 @@
<svg width="256" height="256" version="1.1" viewBox="0 0 67.733 67.733" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(1.2797 0 0 1.2797 -4.4788 -4.4788)" stroke-width="1.0001"><g fill-opacity="0" stroke="#fff" stroke-linecap="round" stroke-width="1.0001"><g stroke-linejoin="round" stroke-opacity=".8" stroke-width="1.0001"><path d="m4 28.965v2"/><path d="m4 40.948v2"/><path d="m4 16.983v2"/></g><g stroke-linejoin="round" stroke-opacity=".8" stroke-width="1.0001"><path d="m28.965 4h2"/><path d="m16.983 4h2"/><path d="m40.948 4.0001h2"/></g><g transform="translate(0 3.3081)" stroke-linejoin="round" stroke-opacity=".8" stroke-width="1.0001"><path d="m28.965 52.623h2"/><path d="m40.948 52.623h2"/><path d="m16.983 52.623h2"/></g><g transform="translate(3.3082)" stroke-linejoin="round" stroke-opacity=".8" stroke-width="1.0001"><path d="m52.623 28.965v2"/><path d="m52.623 16.983v2.0001"/><path d="m52.623 40.948v2.0001"/></g><g stroke-linejoin="round"><path d="m7 4h-3v3" stroke-opacity=".8" stroke-width="1.0001"/><path d="m52.931 4h3v3" stroke-opacity=".8" stroke-width="1.0001"/><path d="m7 55.931h-3v-3" stroke-opacity=".8" stroke-width="1.0001"/></g><path d="m28.311 27.311v2.0001m-1-1h2" stroke-opacity=".8" stroke-width="1.0001"/><path d="m52.931 55.931h3v-3" stroke-linejoin="round" stroke-opacity=".8" stroke-width="1.0001"/></g></g><g transform="matrix(.64211 -.68634 .59172 .63248 35.302 51.828)"><path transform="translate(1.6405e-6)" d="m7 13.5v-7c0-0.82842 0.67157-1.5 1.5-1.5s1.5 0.55521 1.5 1.3836v0.11637 2m0-1e-5v-2c0-2.2857 3-2.2857 3 0v0.52708 1.4729m0 3e-5v-1.4729c0-2.2857 3-2.2857 3 0v0.9015 0.57143m-3 0v-1.4729m0-0.52707v0.52707m3 1.4729v-0.57143-0.9015c0-2.2857-3-2.2857-3 0m3 1.4728v-0.57143c0-2.2857 3-2.2857 3 0m-12 2.5715-2.0041 2.6721c-0.57746 0.77-0.52464 1.842 0.12569 2.5515l3.7839 4.1279c0.37882 0.4132 0.91276 0.6485 1.4734 0.6485h4.6211c2.4 0 4-1.5 4-4v-8.5714" fill-opacity="0" stroke="#fff" stroke-opacity=".9"/></g></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
import React from 'react' import React, {useRef, useEffect, useState} from 'react'
import {Droppable} from "@hello-pangea/dnd"; import {Droppable} from "@hello-pangea/dnd";
import {cartStyle, compareArraysWithIds} from "./utils"; import {cartStyle, compareArraysWithIds} from "./utils";
import {ProductCartItem} from "./ProductCartItem"; import {ProductCartItem} from "./ProductCartItem";
@ -9,17 +9,17 @@ import {useShopStore} from "./shop_store";
// #!render_count // #!render_count
import {useRenderCount} from "@uidotdev/usehooks"; import {useRenderCount} from "@uidotdev/usehooks";
/**
* Component that displays a list of <ProductCartItem>
*/
export function Cart({crate_index}) { export function Cart({crate_index}) {
// #!render_count // #!render_count
const renderCount = useRenderCount(); const renderCount = useRenderCount();
const containerRef = useRef(null);
const [visiblePlaceholders, setVisiblePlaceholders] = useState(0);
const crate = useShopStore((state) => state.crates[crate_index], (a, b) => { const crate = useShopStore((state) => state.crates[crate_index], (a, b) => {
return compareArraysWithIds(a.items, b.items) && a.occupiedHP === b.occupiedHP && a.crate_mode === b.crate_mode return compareArraysWithIds(a.items, b.items) && a.occupiedHP === b.occupiedHP && a.crate_mode === b.crate_mode
}); });
const crateParams = useShopStore((state) => state.crateParams); const crateParams = useShopStore((state) => state.crateParams);
const isCrate = useShopStore((state) => state.modes_order.includes(state.crates[crate_index].crate_mode));
// #!render_count // #!render_count
console.log("Cart renders: ", renderCount) console.log("Cart renders: ", renderCount)
@ -27,23 +27,43 @@ export function Cart({crate_index}) {
const nbrOccupied = hp_to_slots(crate.occupiedHP); const nbrOccupied = hp_to_slots(crate.occupiedHP);
const nbrSlots = hp_to_slots(crateParams(crate.crate_mode).hp); const nbrSlots = hp_to_slots(crateParams(crate.crate_mode).hp);
useEffect(() => {
const updateVisiblePlaceholders = () => {
if (!containerRef.current) return;
setVisiblePlaceholders(isCrate ?
(nbrSlots - nbrOccupied) :
// 10 is padding and 77 is the actual size with all the margins
Math.max(1, Math.floor((containerRef.current.offsetWidth - 10) / 77) - nbrOccupied));
};
updateVisiblePlaceholders();
const resizeObserver = new ResizeObserver(updateVisiblePlaceholders);
if (containerRef.current) {
resizeObserver.observe(containerRef.current);
}
return () => {
if (containerRef.current) {
resizeObserver.unobserve(containerRef.current);
}
};
}, [nbrOccupied, nbrSlots]);
const products = crate.items.map((item, index) => { const products = crate.items.map((item, index) => {
return ( return (
<ProductCartItem <ProductCartItem
card_index={index} card_index={index}
crate_index={crate_index} crate_index={crate_index}
first={index === 0}
last={index === crate.items.length - 1 && nbrOccupied >= nbrSlots}
key={item.id}/> key={item.id}/>
); );
}); });
return ( return (
<Droppable droppableId={crate.id} direction="horizontal"> <Droppable droppableId={crate.id} direction="horizontal">
{(provided, snapshot) => ( {(provided, snapshot) => (
<div <div
ref={provided.innerRef} ref={(element) => {
containerRef.current = element;
provided.innerRef(element);
}}
{...provided.droppableProps} {...provided.droppableProps}
style={cartStyle( style={cartStyle(
provided.droppableProps.style, provided.droppableProps.style,
@ -60,11 +80,10 @@ export function Cart({crate_index}) {
)} )}
<FakePlaceholder <FakePlaceholder
nToDraw={nbrSlots - nbrOccupied} nToDraw={visiblePlaceholders}
isDraggingOver={snapshot.isDraggingOver}/> isDraggingOver={snapshot.isDraggingOver}/>
</div> </div>
)} )}
</Droppable> </Droppable>
); );
} }

View File

@ -0,0 +1,59 @@
import React from 'react'
import {Droppable} from "@hello-pangea/dnd";
import {cartStyle, compareArraysWithIds} from "./utils";
import {ProductCartItemHorizontal} from "./ProductCartItemHorizontal";
import {HORIZONTAL_CART_MARKER, useShopStore} from "./shop_store";
// #!render_count
import {useRenderCount} from "@uidotdev/usehooks";
/**
* Component that displays a list of <ProductCartItem>
*/
export function CartHorizontal({crate_index}) {
// #!render_count
const renderCount = useRenderCount();
const crate = useShopStore((state) => state.crates[crate_index], (a, b) => {
return compareArraysWithIds(a.h_items, b.h_items)
});
// #!render_count
console.log("CartHorizontal renders: ", renderCount)
const products = crate.h_items.map((item, index) => {
return (
<ProductCartItemHorizontal
card_index={index}
crate_index={crate_index}
key={item.id}/>
);
});
return (
<Droppable droppableId={crate.id+HORIZONTAL_CART_MARKER} direction="vertical">
{(provided, snapshot) => (
<div
ref={provided.innerRef}
{...provided.droppableProps}
style={cartStyle(
provided.droppableProps.style,
snapshot,
)}
className="items-cart-list horizontal">
{products}
{provided.placeholder && (
<div style={{display: 'none'}}>
{provided.placeholder}
</div>
)}
</div>
)}
</Droppable>
);
}

View File

@ -16,7 +16,7 @@ export function Catalog() {
const renderCount = useRenderCount(); const renderCount = useRenderCount();
const data = useShopStore((state) => state.groups); const data = useShopStore((state) => state.groups);
const items = useShopStore((state) => state.cards); const _items = useShopStore((state) => state.cards);
const onClickToggleMobileSideMenu = useShopStore((state) => state.switchSideMenu); const onClickToggleMobileSideMenu = useShopStore((state) => state.switchSideMenu);
const isMobile = useShopStore((state) => state.isMobile); const isMobile = useShopStore((state) => state.isMobile);

View File

@ -4,6 +4,7 @@ import {CrateMode} from "./CrateMode";
import {CrateWarnings} from "./CrateWarnings"; import {CrateWarnings} from "./CrateWarnings";
import {useShopStore} from "./shop_store"; import {useShopStore} from "./shop_store";
import {CrateOptions} from "./CrateOptions"; import {CrateOptions} from "./CrateOptions";
import {CartHorizontal} from "./CartHorizontal";
// #!render_count // #!render_count
import {useRenderCount} from "@uidotdev/usehooks"; import {useRenderCount} from "@uidotdev/usehooks";
@ -28,7 +29,7 @@ export function Crate({crate_index}) {
return ( return (
<div className="crate"> <div className="crate">
{ {
modes_order.includes(crate.crate_mode) ? ( modes_order.includes(crate.crate_mode) && (
<div className="crate-bar d-inline-flex justify-content-between"> <div className="crate-bar d-inline-flex justify-content-between">
<CrateMode crate_index={crate_index}/> <CrateMode crate_index={crate_index}/>
@ -36,13 +37,17 @@ export function Crate({crate_index}) {
Delete crate <img src="/images/shop/icon-remove.svg" alt="remove"/> Delete crate <img src="/images/shop/icon-remove.svg" alt="remove"/>
</div> </div>
</div> </div>
) : <></> )
} }
<div className="crate-products"> <div className="crate-products">
<Cart crate_index={crate_index}/> <Cart crate_index={crate_index}/>
{ !modes_order.includes(crate.crate_mode) &&
<CartHorizontal crate_index={crate_index}/>
}
<CrateWarnings crate_index={crate_index} /> <CrateWarnings crate_index={crate_index} />
<CrateOptions crate_index={crate_index}/> <CrateOptions crate_index={crate_index}/>

View File

@ -12,7 +12,7 @@ export function FakePlaceholder({isDraggingOver, nToDraw}) {
<div key={i} style={{ <div key={i} style={{
display: isDraggingOver ? 'none' : 'block', display: isDraggingOver ? 'none' : 'block',
border: '1px dashed #ccc', border: '1px dashed #ccc',
width: '45px', width: '69px',
marginBottom: '5px', marginBottom: '5px',
}}></div> }}></div>
); );

View File

@ -1,21 +1,23 @@
import {DialogPopup} from "./options/DialogPopup"; import {DialogPopup} from "./options/DialogPopup";
import React from "react"; import React from "react";
import {useShopStore} from "./shop_store"; import {useShopStore, whichItems} from "./shop_store";
import {SummaryPopup} from "./options/SummaryPopup"; import {SummaryPopup} from "./options/SummaryPopup";
export function OptionsDialogWrapper({crate_index, card_index, first, last}) { export function OptionsDialogWrapper({crate_index, card_index, horizontal}) {
const whichH = whichItems(horizontal);
const crate_id = useShopStore((state) => state.crates[crate_index].id); const crate_id = useShopStore((state) => state.crates[crate_index].id);
const options = useShopStore((state) => state.crates[crate_index].items[card_index].options); const use_options = useShopStore((state) => state.crateParams(state.crates[crate_index].crate_mode).options);
const options_data = useShopStore((state) => state.crates[crate_index].items[card_index].options_data); const options = useShopStore((state) => state.crates[crate_index][whichH][card_index][use_options]);
const card_size = useShopStore((state) => state.crates[crate_index].items[card_index].size); const options_data = useShopStore((state) => state.crates[crate_index][whichH][card_index].options_data);
const card_id = useShopStore((state) => state.crates[crate_index].items[card_index].id); const card_id = useShopStore((state) => state.crates[crate_index][whichH][card_index].id);
const options_class = useShopStore((state) => state.crates[crate_index].items[card_index].options_class); const options_class = useShopStore((state) => state.crates[crate_index][whichH][card_index].options_class);
const sideMenuIsOpen = useShopStore((state) => state.sideMenuIsOpen); const sideMenuIsOpen = useShopStore((state) => state.sideMenuIsOpen);
const _notificationTimer = useShopStore((state) => state.notificationTimer); const _notificationTimer = useShopStore((state) => state.notificationTimer);
const hideNotification = useShopStore((state) => state.hideNotification); const hideNotification = useShopStore((state) => state.hideNotification);
const displayNotification = useShopStore((state) => const displayNotification = useShopStore((state) =>
!!state.notificationHorizontal === !!horizontal &&
state.notificationCrateId === crate_id && state.notificationCrateId === crate_id &&
(state.notificationCardIndex === card_index || (state.crates[crate_index].items.length + (state.notificationCardIndex || -1)) === card_index)); (state.notificationCardIndex === card_index || (state.crates[crate_index][whichH].length + (state.notificationCardIndex || -1)) === card_index));
const onOptionsUpdate = useShopStore((state) => state.updateOptions); const onOptionsUpdate = useShopStore((state) => state.updateOptions);
@ -26,12 +28,10 @@ export function OptionsDialogWrapper({crate_index, card_index, first, last}) {
options_class={options_class} options_class={options_class}
key={"popover" + crate_id +card_id} key={"popover" + crate_id +card_id}
id={"popover"+ crate_id + card_id} id={"popover"+ crate_id + card_id}
big={card_size === "big"}
first={first}
last={last}
sideMenuIsOpen={sideMenuIsOpen} sideMenuIsOpen={sideMenuIsOpen}
onHideNotification={hideNotification} onHideNotification={hideNotification}
displayNotification={displayNotification} displayNotification={displayNotification}
horizontal={horizontal}
target={{ target={{
construct: ((outvar, value) => { construct: ((outvar, value) => {
// #!options_log // #!options_log
@ -44,17 +44,19 @@ export function OptionsDialogWrapper({crate_index, card_index, first, last}) {
console.log("update", outvar, value, options_data); console.log("update", outvar, value, options_data);
if (outvar in options_data) options_data[outvar] = value; if (outvar in options_data) options_data[outvar] = value;
onOptionsUpdate(crate_id, card_index, {[outvar]: value}); onOptionsUpdate(crate_id, card_index, {[outvar]: value}, horizontal);
}) })
}} }}
/> />
) )
} }
export function OptionsSummaryWrapper({crate_index, card_index}) { export function OptionsSummaryWrapper({crate_index, card_index, horizontal}) {
const card_id = useShopStore((state) => state.crates[crate_index].items[card_index].id); const whichH = whichItems(horizontal);
const options = useShopStore((state) => state.crates[crate_index].items[card_index].options); const card_id = useShopStore((state) => state.crates[crate_index][whichH][card_index].id);
const options_data = useShopStore((state) => state.crates[crate_index].items[card_index].options_data); const use_options = useShopStore((state) => state.crateParams(state.crates[crate_index].crate_mode).options);
const options = useShopStore((state) => state.crates[crate_index][whichH][card_index][use_options]);
const options_data = useShopStore((state) => state.crates[crate_index][whichH][card_index].options_data);
return ( return (
<SummaryPopup id={card_id + "options"} options={options} <SummaryPopup id={card_id + "options"} options={options}

View File

@ -8,7 +8,6 @@ import {RFQFeedback} from "./RFQFeedback";
// #!render_count // #!render_count
import {useRenderCount} from "@uidotdev/usehooks"; import {useRenderCount} from "@uidotdev/usehooks";
import {OrderOptions} from "./OrderOptions";
/** /**
* Component that renders all things for order. * Component that renders all things for order.

View File

@ -13,7 +13,7 @@ import {useRenderCount} from "@uidotdev/usehooks";
* Component that renders a product. * Component that renders a product.
* Used in the crate * Used in the crate
*/ */
export function ProductCartItem({card_index, crate_index, first, last}) { export function ProductCartItem({card_index, crate_index}) {
// #!render_count // #!render_count
const renderCount = useRenderCount(); const renderCount = useRenderCount();
@ -24,8 +24,12 @@ export function ProductCartItem({card_index, crate_index, first, last}) {
const card_show_warnings = useShopStore(state => state.crates[crate_index].items[card_index].show_warnings, compareObjectsEmptiness); const card_show_warnings = useShopStore(state => state.crates[crate_index].items[card_index].show_warnings, compareObjectsEmptiness);
const card_counted_resources = useShopStore(state => state.crates[crate_index].items[card_index].counted_resources, compareObjectsEmptiness); const card_counted_resources = useShopStore(state => state.crates[crate_index].items[card_index].counted_resources, compareObjectsEmptiness);
const highlighted = useShopStore((state) => state.crates[crate_index].id === state.highlighted.crate && card_index === state.highlighted.card); const highlighted = useShopStore((state) =>
const options_disabled = useShopStore((state) => !!state.crateParams(state.crates[crate_index].crate_mode).warnings_disabled); !state.highlighted.horizontal &&
state.crates[crate_index].id === state.highlighted.crate &&
card_index === state.highlighted.card);
const warnings_disabled = useShopStore((state) => !!state.crateParams(state.crates[crate_index].crate_mode).warnings_disabled);
const use_options = useShopStore((state) => state.crateParams(state.crates[crate_index].crate_mode).options);
const crate_id = useShopStore((state) => state.crates[crate_index].id); const crate_id = useShopStore((state) => state.crates[crate_index].id);
const setHighlight = useShopStore((state) => state.highlightCard); const setHighlight = useShopStore((state) => state.highlightCard);
const removeHighlight = useShopStore((state) => state.highlightReset); const removeHighlight = useShopStore((state) => state.highlightReset);
@ -35,9 +39,9 @@ export function ProductCartItem({card_index, crate_index, first, last}) {
console.log("ProductCartItem renders: ", renderCount) console.log("ProductCartItem renders: ", renderCount)
const options = !options_disabled && card && card.options && card.options.length > 0; const options = use_options && card && card[use_options] && card[use_options].length > 0;
const warnings = !options_disabled && card_show_warnings && card_show_warnings.length > 0; const warnings = !warnings_disabled && card_show_warnings && card_show_warnings.length > 0;
const resources = !options_disabled && card_counted_resources && card_counted_resources.length > 0; const resources = !warnings_disabled && card_counted_resources && card_counted_resources.length > 0;
return ( return (
<Draggable draggableId={card.id} index={card_index}> <Draggable draggableId={card.id} index={card_index}>
@ -57,7 +61,7 @@ export function ProductCartItem({card_index, crate_index, first, last}) {
true true
) )
}} }}
onMouseEnter={() => setHighlight(crate_id, card_index)} onMouseEnter={() => setHighlight(crate_id, card_index, false)}
onMouseLeave={removeHighlight} onMouseLeave={removeHighlight}
> >
@ -72,8 +76,6 @@ export function ProductCartItem({card_index, crate_index, first, last}) {
<OptionsDialogWrapper <OptionsDialogWrapper
crate_index={crate_index} crate_index={crate_index}
card_index={card_index} card_index={card_index}
first={first}
last={last}
/> />
)} )}
</div> </div>

View File

@ -0,0 +1,96 @@
import React from 'react'
import {Draggable} from "@hello-pangea/dnd";
import {productStyle} from "./utils";
import {useShopStore} from "./shop_store";
import {OptionsDialogWrapper} from "./OptionsWrapper";
// #!render_count
import {useRenderCount} from "@uidotdev/usehooks";
/**
* Component that renders a product.
* Used in the crate
*/
export function ProductCartItemHorizontal({card_index, crate_index}) {
// #!render_count
const renderCount = useRenderCount();
const card = useShopStore((state) => state.crates[crate_index].h_items[card_index],
(a, b) => a.id === b.id);
const highlighted = useShopStore((state) =>
!!state.highlighted.horizontal &&
state.crates[crate_index].id === state.highlighted.crate &&
card_index === state.highlighted.card
);
const use_options = useShopStore((state) => state.crateParams(state.crates[crate_index].crate_mode).options);
const crate_id = useShopStore((state) => state.crates[crate_index].id);
const setHighlight = useShopStore((state) => state.highlightCard);
const removeHighlight = useShopStore((state) => state.highlightReset);
const onCardRemove = useShopStore((state) => state.deleteCard);
// #!render_count
console.log("ProductCartItem renders: ", renderCount)
const options = use_options && card && card[use_options] && card[use_options].length > 0;
return (
<Draggable draggableId={card.id} index={card_index}>
{(provided, snapshot) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={{
...productStyle(
provided.draggableProps.style,
snapshot,
true,
!!highlighted,
false,
true
)
}}
onMouseEnter={() => setHighlight(crate_id, card_index, true)}
onMouseLeave={removeHighlight}
>
{/* warning container */}
<div className="progress-container warning d-flex justify-content-evenly">
{options && (
<OptionsDialogWrapper
crate_index={crate_index}
card_index={card_index}
horizontal={true}
/>
)}
</div>
<div
className="product-name"
onMouseEnter={() => setHighlight(crate_id, card_index, true)}
onClick={() => setHighlight(crate_id, card_index, true)}
>
{`${card.name_number} ${card.name} ${card.name_codename}`}
</div>
{/* remove container */}
<div
style={{'display': 'flex'}}
className="removeHorizontal"
onClick={() => onCardRemove(crate_id, card_index, true)}>
<img src="/images/shop/icon-remove.svg" alt="rm"/>
</div>
</div>
)}
</Draggable>
);
}

View File

@ -7,6 +7,8 @@ import {useShopStore} from "./shop_store";
import {useRenderCount} from "@uidotdev/usehooks"; import {useRenderCount} from "@uidotdev/usehooks";
const DNDIcon = "/images/shop/icon-drag-and-drop.svg";
function DatasheetLink({datasheet_file, datasheet_name}) { function DatasheetLink({datasheet_file, datasheet_name}) {
return datasheet_file && datasheet_name && (<div className="ds"> return datasheet_file && datasheet_name && (<div className="ds">
<span className='doc-icon'></span> <span className='doc-icon'></span>
@ -76,11 +78,12 @@ export function ProductItem({card_index}) {
snapshot, snapshot,
true, // hack: remove weird animation after a drop true, // hack: remove weird animation after a drop
)} )}
src={card.image}/> className={card.image ? "" : "default-icon"}
src={card.image || DNDIcon}/>
{/* Allows to simulate a clone */} {/* Allows to simulate a clone */}
{snapshot.isDragging && ( {snapshot.isDragging && (
<img className="simclone" src={card.image}/> <img className={"simclone " + (card.image ? "" : "default-icon")} src={card.image || DNDIcon}/>
)} )}
</React.Fragment> </React.Fragment>
)} )}

View File

@ -15,6 +15,7 @@ export function SummaryCrate({crate_index}) {
const crate_id = useShopStore((state) => state.crates[crate_index].id); const crate_id = useShopStore((state) => state.crates[crate_index].id);
const crate_len = useShopStore((state) => state.crates[crate_index].items.length); const crate_len = useShopStore((state) => state.crates[crate_index].items.length);
const crate_h_len = useShopStore((state) => state.crates[crate_index].h_items.length);
// #!render_count // #!render_count
console.log("SummaryCrate renders: ", renderCount) console.log("SummaryCrate renders: ", renderCount)
@ -28,6 +29,14 @@ export function SummaryCrate({crate_index}) {
<SummaryCrateCard crate_index={crate_index} card_index={index} key={"summary_crate_" + crate_id + "_" +index} /> <SummaryCrateCard crate_index={crate_index} card_index={index} key={"summary_crate_" + crate_id + "_" +index} />
)} )}
{range(0, crate_h_len).map((index, _i) =>
<SummaryCrateCard
crate_index={crate_index}
card_index={index}
horizontal={true}
key={"summary_crate_h_" + crate_id + "_" +index} />
)}
<SummaryCratePricedOptions crate_index={crate_index}/> <SummaryCratePricedOptions crate_index={crate_index}/>
</tbody> </tbody>
) )

View File

@ -1,45 +1,56 @@
import {compareObjectsEmptiness, formatMoney} from "./utils"; import {compareObjectsEmptiness, formatMoney} from "./utils";
import {WarningIndicator} from "./CardWarnings"; import {WarningIndicator} from "./CardWarnings";
import React from "react"; import React from "react";
import {useShopStore} from "./shop_store"; import {useShopStore, whichItems} from "./shop_store";
import {OptionsSummaryWrapper} from "./OptionsWrapper"; import {OptionsSummaryWrapper} from "./OptionsWrapper";
// #!render_count // #!render_count
import {useRenderCount} from "@uidotdev/usehooks"; import {useRenderCount} from "@uidotdev/usehooks";
export function SummaryCrateCard({crate_index, card_index}) { export function SummaryCrateCard({crate_index, card_index, horizontal}) {
// #!render_count // #!render_count
const renderCount = useRenderCount(); const renderCount = useRenderCount();
const whichH = whichItems(horizontal);
const currency = useShopStore((state) => state.currency); const currency = useShopStore((state) => state.currency);
const deleteCard = useShopStore((state) => state.deleteCard); const deleteCard = useShopStore((state) => state.deleteCard);
const setHighlight = useShopStore((state) => state.highlightCard); const setHighlight = useShopStore((state) => state.highlightCard);
const resetHighlight = useShopStore((state) => state.highlightReset); const resetHighlight = useShopStore((state) => state.highlightReset);
const highlighted = useShopStore((state) => state.crates[crate_index].id === state.highlighted.crate && card_index === state.highlighted.card); const highlighted = useShopStore((state) =>
state.highlighted.horizontal === horizontal &&
state.crates[crate_index].id === state.highlighted.crate &&
card_index === state.highlighted.card);
const crate_id = useShopStore((state) => state.crates[crate_index].id); const crate_id = useShopStore((state) => state.crates[crate_index].id);
const card = useShopStore((state) => state.crates[crate_index].items[card_index], const card = useShopStore((state) =>
state.crates[crate_index][whichH][card_index],
(a, b) => a.id === b.id); (a, b) => a.id === b.id);
const card_show_warnings = useShopStore(state => state.crates[crate_index].items[card_index].show_warnings, compareObjectsEmptiness); // additional hooks for updating warning and options
const card_options_data = useShopStore(state => state.crates[crate_index].items[card_index].options_data, compareObjectsEmptiness); const card_show_warnings = useShopStore(state =>
const options_disabled = useShopStore((state) => !!state.crateParams(state.crates[crate_index].crate_mode).warnings_disabled); state.crates[crate_index][whichH][card_index].show_warnings,
compareObjectsEmptiness);
const card_options_data = useShopStore(state =>
state.crates[crate_index][whichH][card_index].options_data,
compareObjectsEmptiness);
const warnings_disabled = useShopStore((state) => !!state.crateParams(state.crates[crate_index].crate_mode).warnings_disabled);
const use_options = useShopStore((state) => state.crateParams(state.crates[crate_index].crate_mode).options);
// #!render_count // #!render_count
console.log("SummaryCrateCard renders: ", renderCount) console.log("SummaryCrateCard renders: ", renderCount)
const options = !options_disabled && card && card.options && card.options.length > 0; const options = use_options && card && card[use_options] && card[use_options].length > 0;
const options_data = !options_disabled && card_options_data && Object.keys(card_options_data).length > 0; const options_data = card_options_data && Object.keys(card_options_data).length > 0;
const warnings = !options_disabled && card_show_warnings && card_show_warnings.length > 0; const warnings = !warnings_disabled && card_show_warnings && card_show_warnings.length > 0;
return ( return (
<tr <tr
key={"summary_crate_" + crate_id + "_" + card_index} key={"summary_crate_" + crate_id + "_" + card_index}
className={`hoverable ${highlighted ? 'selected' : ''}`} className={`hoverable ${highlighted ? 'selected' : ''}`}
onClick={() => setHighlight(crate_id, card_index)} onClick={() => setHighlight(crate_id, card_index, horizontal)}
onMouseEnter={() => setHighlight(crate_id, card_index)} onMouseEnter={() => setHighlight(crate_id, card_index, horizontal)}
onMouseLeave={() => resetHighlight()}> onMouseLeave={() => resetHighlight()}>
<td className="item-card-name tabbed"> <td className="item-card-name tabbed">
<div>{`${card.name_number} ${card.name} ${card.name_codename}`}</div> <div>{`${card.name_number} ${card.name} ${card.name_codename}`}</div>
@ -49,7 +60,7 @@ export function SummaryCrateCard({crate_index, card_index}) {
<div className="d-inline-flex align-content-center"> <div className="d-inline-flex align-content-center">
{`${currency} ${formatMoney(card.price)}`} {`${currency} ${formatMoney(card.price)}`}
<button onClick={() => deleteCard(crate_id, card_index)}> <button onClick={() => deleteCard(crate_id, card_index, horizontal)}>
<img src="/images/shop/icon-remove.svg" className="d-block"/> <img src="/images/shop/icon-remove.svg" className="d-block"/>
</button> </button>
@ -64,7 +75,10 @@ export function SummaryCrateCard({crate_index, card_index}) {
}}>&nbsp;</span> }}>&nbsp;</span>
))} ))}
{((options && options_data) ? ( {((options && options_data) ? (
<OptionsSummaryWrapper crate_index={crate_index} card_index={card_index}/> <OptionsSummaryWrapper
crate_index={crate_index}
card_index={card_index}
horizontal={horizontal}/>
) : ( ) : (
<span style={{ <span style={{
'display': 'inline-block', 'display': 'inline-block',

View File

@ -1,7 +1,6 @@
import {formatMoney} from "./utils";
import React from "react"; import React from "react";
import {useShopStore} from "./shop_store"; import {useShopStore} from "./shop_store";
import {ProcessOptions, ProcessOptionsToData} from "./options/Options"; import {ProcessOptions} from "./options/Options";
// #!render_count // #!render_count
import {useRenderCount} from "@uidotdev/usehooks"; import {useRenderCount} from "@uidotdev/usehooks";

View File

@ -21,10 +21,13 @@ export function validateJSON(description) {
try { try {
for (const crate of crates_raw) { for (const crate of crates_raw) {
if (!crate.type || !crate.items || !crate.options || !(crate.type in crate_modes)) return false; if (!crate.type || !crate.items || !crate.h_items || !crate.options || !(crate.type in crate_modes)) return false;
for (const card of crate.items) { for (const card of crate.items) {
if (!(card.pn in pn_to_card) || card.options === undefined) return false; if (!(card.pn in pn_to_card) || card.options === undefined) return false;
} }
for (const card of crate.h_items) {
if (!(card.pn in pn_to_card) || card.options === undefined) return false;
}
} }
} catch (e) { } catch (e) {
return false; return false;
@ -50,6 +53,11 @@ export function JSONToCrates(description) {
id: uuidv4(), id: uuidv4(),
options_data: card.options || {} options_data: card.options || {}
}))), }))),
h_items: Array.from(crate.h_items.map((card, _i) => ({
...pn_to_card(card.pn),
id: uuidv4(),
options_data: card.options || {}
}))),
warnings: [], warnings: [],
occupiedHP: 0, occupiedHP: 0,
}))); })));
@ -65,12 +73,17 @@ export function CratesToJSON(crates) {
const crateOptions = useShopStore.getState().crate_options; const crateOptions = useShopStore.getState().crate_options;
const orderOptions = useShopStore.getState().order_options; const orderOptions = useShopStore.getState().order_options;
const orderOptionsData = useShopStore.getState().order_options_data; const orderOptionsData = useShopStore.getState().order_options_data;
const crateParams = useShopStore.getState().crateParams;
return JSON.stringify({ return JSON.stringify({
// additional fields can go here // additional fields can go here
crates: Array.from(crates.map((crate, _i) => ({ crates: Array.from(crates.map((crate, _i) => ({
items: Array.from(crate.items.map((card, _) => ({ items: Array.from(crate.items.map((card, _) => ({
pn: card.name_number, pn: card.name_number,
options: (card.options_data && card.options) ? FilterOptions(card.options, card.options_data) : null options: (card.options_data && card[crateParams(crate.crate_mode).options]) ? FilterOptions(card[crateParams(crate.crate_mode).options], card.options_data) : null
}))),
h_items: Array.from(crate.h_items.map((card, _) => ({
pn: card.name_number,
options: (card.options_data && card[crateParams(crate.crate_mode).options]) ? FilterOptions(card[crateParams(crate.crate_mode).options], card.options_data) : null
}))), }))),
type: crate.crate_mode, type: crate.crate_mode,
options: FilterOptions(crateOptions, crate.options_data) options: FilterOptions(crateOptions, crate.options_data)

View File

@ -1,46 +1,56 @@
import React, {useState} from "react"; import React, {useRef, useState} from "react";
import {useClickAway} from "./useClickAway";
import {ProcessOptions} from "./Options"; import {ProcessOptions} from "./Options";
import {Notification} from "./Notification"; import {Notification} from "./Notification";
import {Overlay} from "react-bootstrap";
export function DialogPopup({options, data, target, id, big, first, last, options_class, export function DialogPopup({options, data, target, id, options_class,
sideMenuIsOpen, displayNotification, onHideNotification}) { sideMenuIsOpen, displayNotification, onHideNotification, horizontal}) {
const [show, setShow] = useState(false); const [show, setShow] = useState(false);
const ref = useClickAway((e) => { const ref = useRef(null);
if (e.type === "mousedown") // ignore touchstart
setShow(false) let div_classes = `overlayVariant border rounded ${options_class || ""}`;
}
);
let div_classes = `overlayVariant border rounded ${big ? "overlay-bigcard" : "overlay-smallcard"} ${(!big && first) ? "overlay-first" : ""} ${(!big && last && !first) ? "overlay-last" : ""} ${options_class || ""}`;
const handleClick = (_event) => { const handleClick = (_event) => {
setShow(!show); setShow(!show);
}; };
return ( return (<>
<div ref={ref}> <Notification
<Notification id={"processed_options_notification" + id}
id={"processed_options_notification" + id} tip="Customization options available"
tip="Customization options available" sideMenuIsOpen={sideMenuIsOpen}
sideMenuIsOpen={sideMenuIsOpen} show={displayNotification}
show={displayNotification} onHide={onHideNotification}
onHide={onHideNotification} content={
content={ <img className="alert-info" ref={ref}
<img className="alert-info" src={show ? "/images/shop/icon-close.svg" : "/images/shop/icon-customize.svg"}
src={show ? "/images/shop/icon-close.svg" : "/images/shop/icon-customize.svg"} onClick={handleClick}/>
onClick={handleClick}/> }
} />
/> <Overlay target={ref.current}
<div style={{'display': show ? 'flex' : 'none'}} className={div_classes}> show={show}
<ProcessOptions placement={horizontal ? "right" : "bottom"}
options={options} onHide={() => setShow(false)}
data={data} rootClose={true}>
key={"processed_options_" + id} {({
id={"processed_options_" + id} placement: _placement,
target={target} arrowProps: _arrowProps,
/> show: _show,
</div> popper: _popper,
</div> hasDoneInitialMeasure: _hasDoneInitialMeasure,
); ...props
}) => (
<div style={{'display': show ? 'flex' : 'none', ...props.style}} {...props} className={div_classes}>
<ProcessOptions
options={options}
data={data}
key={"processed_options_" + id}
id={"processed_options_" + id}
target={target}
/>
</div>
)}
</Overlay>
</>);
} }

View File

@ -1,13 +1,13 @@
const ipv4 = (params) => { const ipv4 = (_params) => {
const ipv4WithMaskPattern = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\/(0|1[0-9]|2[0-9]|3[0-2]|[0-9])$/; const ipv4WithMaskPattern = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\/(0|1[0-9]|2[0-9]|3[0-2]|[0-9])$/;
return (text) => { return (text) => {
return ipv4WithMaskPattern.test(text); return ipv4WithMaskPattern.test(text);
} }
} }
const ipv6 = (params) => { const ipv6 = (_params) => {
const ipv6WithMaskPattern = /(^\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\s*$)|(^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(\/(\d{1,2}|1[0-1]\d|12[0-8]))(%.+)?\s*$)/; const ipv6WithMaskPattern = /(^\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\s*$)|(^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(\/(\d{1,2}|1[0-1]\d|12[0-8]))(%.+)?\s*$)/;
@ -16,6 +16,35 @@ const ipv6 = (params) => {
} }
} }
const hostname = (_params) => {
const maxHostnameLength = 253;
const maxLabelLength = 63;
const labelRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/;
return (text) => {
if (text.length > maxHostnameLength) {
return false;
}
const labels = text.split('.');
for (const label of labels) {
if (label.length < 1
|| label.length > maxLabelLength
|| !labelRegex.test(label)) {
return false;
}
}
return true;
}
}
const ipv4OrHost = (params) => {
const hostnameLocal = hostname(params);
const ipv4Local = ipv4(params);
return (text) => {
return ipv4Local(text) || hostnameLocal(text);
}
}
const ipv4or6 = (params) => { const ipv4or6 = (params) => {
const ipv4Local = ipv4(params); const ipv4Local = ipv4(params);
const ipv6Local = ipv6(params); const ipv6Local = ipv6(params);
@ -47,5 +76,7 @@ export const Validation = {
ipv4: ipv4, ipv4: ipv4,
ipv6: ipv6, ipv6: ipv6,
ipv4or6: ipv4or6, ipv4or6: ipv4or6,
hostname: hostname,
ipv4OrHost: ipv4OrHost,
frequency: frequency frequency: frequency
}; };

View File

@ -2,7 +2,7 @@
import {createWithEqualityFn} from "zustand/traditional"; import {createWithEqualityFn} from "zustand/traditional";
import {DATA as shared_data, itemsUnfoldedList, API_RFQ} from "./utils"; import {DATA as shared_data, itemsUnfoldedList, API_RFQ} from "./utils";
import {FillExtCrateData, FillExtOrderData, true_type_of} from "./options/utils"; import {FillExtCrateData, FillExtOrderData} from "./options/utils";
import {v4 as uuidv4} from "uuid"; import {v4 as uuidv4} from "uuid";
import {FillResources} from "./count_resources"; import {FillResources} from "./count_resources";
import {FillExtCardData} from "./options/utils"; import {FillExtCardData} from "./options/utils";
@ -13,12 +13,24 @@ import {ProcessOptionsToData} from "./options/Options";
import {DomainedRFQMessages} from "./Domained"; import {DomainedRFQMessages} from "./Domained";
export const HORIZONTAL_CART_MARKER = "_h";
const cards_to_pn_map = (cards) => { const cards_to_pn_map = (cards) => {
let result = {}; let result = {};
Object.entries(cards).forEach(([key, card], _i) => { result[card.name_number] = key}) Object.entries(cards).forEach(([key, card], _i) => { result[card.name_number] = key})
return result; return result;
}; };
const toArray = (arg) => {
return Array.isArray(arg) ? arg : [arg];
};
const unwrapCrateId = (crate_id= "") => {
return crate_id.endsWith(HORIZONTAL_CART_MARKER) ? [true, crate_id.substring(0, crate_id.length - HORIZONTAL_CART_MARKER.length)] : [false, crate_id]
}
export const whichItems = (horizontal = false) => horizontal ? "h_items" : "items"
const useCatalog = ((set, get) => ({ const useCatalog = ((set, get) => ({
cards: shared_data.items, cards: shared_data.items,
groups: shared_data.columns.catalog, groups: shared_data.columns.catalog,
@ -33,21 +45,24 @@ const useCatalog = ((set, get) => ({
const useOptionsNotification = ((set, get) => ({ const useOptionsNotification = ((set, get) => ({
notificationCrateId: null, notificationCrateId: null,
notificationCardIndex: null, notificationCardIndex: null,
notificationHorizontal: false,
notificationTimer: null, notificationTimer: null,
_showNotification: (crate_id, card_index) => set(state => ({ _showNotification: (crate_id, card_index, horizontal) => set(state => ({
notificationCrateId: crate_id, notificationCrateId: crate_id,
notificationCardIndex: card_index, notificationCardIndex: card_index,
notificationHorizontal: horizontal,
notificationTimer: setTimeout(() => { notificationTimer: setTimeout(() => {
state.hideNotification() state.hideNotification()
}, 5000) }, 5000)
})), })),
showNotification: (crate_id, card_index) => { showNotification: (crate_id, card_index, horizontal) => {
get().hideNotification() get().hideNotification()
setTimeout(() => get()._showNotification(crate_id, card_index), 100); setTimeout(() => get()._showNotification(crate_id, card_index, horizontal), 100);
}, },
hideNotification: () => set(state => ({ hideNotification: () => set(state => ({
notificationCrateId: null, notificationCrateId: null,
notificationCardIndex: null, notificationCardIndex: null,
notificationHorizontal: false,
notificationTimer: (state.notificationTimer && clearTimeout(state.notificationTimer)) || null, notificationTimer: (state.notificationTimer && clearTimeout(state.notificationTimer)) || null,
})) }))
})); }));
@ -211,7 +226,7 @@ const useImportJSON = ((set, get) => ({
get().fillExtCrateData(crate.id); get().fillExtCrateData(crate.id);
}); });
get()._updateTotalOrderPrice(); get()._updateTotalOrderPrice();
get().showNotification(get().active_crate, null); get().showNotification(get().active_crate, null, false);
}, },
updateImportDescription: (new_description) => set(state => ({ updateImportDescription: (new_description) => set(state => ({
importValue: { importValue: {
@ -328,15 +343,17 @@ const useSubmitForm = ((set, get) => ({
const useHighlighted = ((set, get) => ({ const useHighlighted = ((set, get) => ({
highlighted: { highlighted: {
crate: "", crate: "",
card: 0 card: 0,
horizontal: false
}, },
highlightedTimer: null, highlightedTimer: null,
// #!if disable_card_highlight === false // #!if disable_card_highlight === false
highlightCard: (crate_id, index) => set(state => ({ highlightCard: (crate_id, index, horizontal) => set(state => ({
highlighted: { highlighted: {
crate: crate_id, crate: crate_id,
card: index card: index,
horizontal: horizontal
}, },
highlightedTimer: (!!state.highlightedTimer ? clearTimeout(state.highlightedTimer) : null) || (state.isTouch && setTimeout(() => { highlightedTimer: (!!state.highlightedTimer ? clearTimeout(state.highlightedTimer) : null) || (state.isTouch && setTimeout(() => {
get().highlightReset() get().highlightReset()
@ -345,7 +362,8 @@ const useHighlighted = ((set, get) => ({
highlightReset: () => set(state => ({ highlightReset: () => set(state => ({
highlighted: { highlighted: {
crate: "", crate: "",
card: 0 card: 0,
horizontal: false
}, },
highlightedTimer: !!state.highlightedTimer ? clearTimeout(state.highlightedTimer) : null highlightedTimer: !!state.highlightedTimer ? clearTimeout(state.highlightedTimer) : null
})), })),
@ -384,17 +402,18 @@ const useCart = ((set, get) => ({
}) })
})), })),
setActiveCrate: (id) => set(state => ({active_crate: id})), setActiveCrate: (id) => set(state => ({active_crate: id})),
_addCardFromCatalog: (crate_to, index_from, index_to) => set(state => { _addCardFromCatalog: (crate_to, index_from, index_to, horizontal) => set(state => {
const take_from = (true_type_of(index_from) === "array" ? index_from : [index_from]).map((item, _i) => (state.cards_list[item])); const whichH = whichItems(horizontal)
const take_from = toArray(index_from).map((item, _i) => (state.cards_list[item]));
const dest = crate_to || state.active_crate; const dest = crate_to || state.active_crate;
if (!dest) return {}; if (!dest) return {};
return { return {
crates: state.crates.map((crate, _i) => { crates: state.crates.map((crate, _i) => {
if (dest === crate.id) { if (dest === crate.id) {
index_to = index_to != null ? index_to : crate.items.length; index_to = index_to != null ? index_to : crate[whichH].length;
return { return {
...crate, ...crate,
items: crate.items.toSpliced(index_to, 0, ...take_from.map((card_name, _) => { [whichH]: crate[whichH].toSpliced(index_to, 0, ...take_from.map((card_name, _) => {
return {...state.cards[card_name], id: uuidv4()} return {...state.cards[card_name], id: uuidv4()}
})) }))
} }
@ -402,39 +421,41 @@ const useCart = ((set, get) => ({
}) })
} }
}), }),
_moveCard: (crate_from, index_from, crate_to, index_to) => set(state => { _moveCard: (crate_from, index_from, crate_to, index_to, horizontal) => set(state => {
const the_card = state.crates.find((crate, _) => crate_from === crate.id ).items[index_from]; const whichH = whichItems(horizontal)
const the_card = state.crates.find((crate, _) => crate_from === crate.id )[whichH][index_from];
return { return {
crates: state.crates.map((crate, _i) => { crates: state.crates.map((crate, _i) => {
if (crate_to === crate_from && crate_to === crate.id) { if (crate_to === crate_from && crate_to === crate.id) {
let items_copy = Array.from(crate.items); let items_copy = Array.from(crate[whichH]);
let item = items_copy.splice(index_from, 1)[0] let item = items_copy.splice(index_from, 1)[0]
items_copy.splice(index_to, 0, item).filter((item, _) => !!item) items_copy.splice(index_to, 0, item).filter((item, _) => !!item)
return { return {
...crate, ...crate,
items: items_copy [whichH]: items_copy
} }
} else if (crate_to === crate.id) { } else if (crate_to === crate.id) {
return { return {
...crate, ...crate,
items: crate.items.toSpliced(index_to, 0, the_card) [whichH]: crate[whichH].toSpliced(index_to, 0, the_card)
} }
} else if (crate_from === crate.id) { } else if (crate_from === crate.id) {
return { return {
...crate, ...crate,
items: crate.items.toSpliced(index_to, 1) [whichH]: crate[whichH].toSpliced(index_to, 1)
} }
} }
else return crate; else return crate;
}) })
} }
}), }),
_deleteCard: (crate_id, index) => set(state => ({ _deleteCard: (crate_id, index, horizontal) => set(state => ({
crates: state.crates.map((crate, _i) => { crates: state.crates.map((crate, _i) => {
if (crate_id === crate.id) { if (crate_id === crate.id) {
const whichH = whichItems(horizontal)
return { return {
...crate, ...crate,
items: crate.items.toSpliced(index, 1) [whichH]: crate[whichH].toSpliced(index, 1)
} }
} }
else return crate; else return crate;
@ -445,7 +466,8 @@ const useCart = ((set, get) => ({
if (id === crate.id) { if (id === crate.id) {
return { return {
...crate, ...crate,
items: [] items: [],
h_items: []
} }
} }
else return crate; else return crate;
@ -454,10 +476,11 @@ const useCart = ((set, get) => ({
clearAll: () => set(state => ({ clearAll: () => set(state => ({
crates: state._defaultCrates crates: state._defaultCrates
})), })),
_updateOptions: (crate_id, index, new_options) => set(state => ({ _updateOptions: (crate_id, index, new_options, horizontal) => set(state => ({
crates: state.crates.map((crate, _i) => { crates: state.crates.map((crate, _i) => {
if (crate_id === crate.id) { if (crate_id === crate.id) {
let itemsCopy = Array.from(crate.items); const whichH = whichItems(horizontal)
let itemsCopy = Array.from(crate[whichH]);
itemsCopy[index] = { itemsCopy[index] = {
...itemsCopy[index], ...itemsCopy[index],
options_data: { options_data: {
@ -466,7 +489,7 @@ const useCart = ((set, get) => ({
}}; }};
return { return {
...crate, ...crate,
items: itemsCopy [whichH]: itemsCopy
} }
} }
else return crate; else return crate;
@ -476,7 +499,7 @@ const useCart = ((set, get) => ({
fillWarnings: (crate_id) => set(state => ({ fillWarnings: (crate_id) => set(state => ({
crates: state.crates.map((crate, _i) => { crates: state.crates.map((crate, _i) => {
if (crate_id === crate.id) { if (crate_id === crate.id) {
//console.log("--- CHECK ALERTS ---") // Warnings for horizontal items are not available
let itemsCopy = Array.from(crate.items); let itemsCopy = Array.from(crate.items);
const disabled = !!get().crateParams(crate.crate_mode).warnings_disabled; const disabled = !!get().crateParams(crate.crate_mode).warnings_disabled;
itemsCopy = FillResources(itemsCopy, disabled); itemsCopy = FillResources(itemsCopy, disabled);
@ -493,20 +516,23 @@ const useCart = ((set, get) => ({
}) })
})), })),
fillExtData: (crate_id) => set(state => ({ fillExtData: (crate_id, horizontal) => set(state => ({
crates: state.crates.map((crate, _i) => { crates: state.crates.map((crate, _i) => {
// horizontal items do not interact with each other for now
if (crate_id === crate.id) { if (crate_id === crate.id) {
let itemsCopy = Array.from(crate.items); const whichH = whichItems(horizontal)
const options_name = state.crateParams(crate.crate_mode).options;
let itemsCopy = Array.from(crate[whichH]);
itemsCopy = itemsCopy.map((item, index) => { itemsCopy = itemsCopy.map((item, index) => {
if (!item.options) return item; if (!item[options_name]) return item;
if (!item.options_data) item.options_data = {}; if (!item.options_data) item.options_data = {};
item.options_data.ext_data = FillExtCardData(itemsCopy, index); item.options_data.ext_data = FillExtCardData(itemsCopy, index);
return item; return item;
}); });
return { return {
...crate, ...crate,
items: Array.from(itemsCopy) [whichH]: Array.from(itemsCopy)
} }
} }
else return crate; else return crate;
@ -519,7 +545,7 @@ const useCart = ((set, get) => ({
sum += get().crate_modes[crate.crate_mode].price; sum += get().crate_modes[crate.crate_mode].price;
const crate_options = ProcessOptionsToData({options: get().crate_prices, data: crate.options_data || {}}); const crate_options = ProcessOptionsToData({options: get().crate_prices, data: crate.options_data || {}});
sum += crate_options ? crate_options.reduce((accumulator, currentValue) => accumulator+currentValue.price, 0) : 0; sum += crate_options ? crate_options.reduce((accumulator, currentValue) => accumulator+currentValue.price, 0) : 0;
crate.items.forEach((item, _) => { crate.items.concat(crate.h_items).forEach((item, _) => {
sum += item.price; sum += item.price;
}); });
}); });
@ -534,6 +560,7 @@ const useCart = ((set, get) => ({
const crate_id = "crate" + get().crates.length; const crate_id = "crate" + get().crates.length;
get()._newCrate(crate_id) get()._newCrate(crate_id)
get().fillExtData(crate_id); get().fillExtData(crate_id);
get().fillExtData(crate_id, true);
get().fillExtCrateData(crate_id); get().fillExtCrateData(crate_id);
get().fillOrderExtData(); get().fillOrderExtData();
get().fillWarnings(crate_id); get().fillWarnings(crate_id);
@ -543,6 +570,7 @@ const useCart = ((set, get) => ({
setCrateMode: (id, mode) => { setCrateMode: (id, mode) => {
get()._setCrateMode(id, mode) get()._setCrateMode(id, mode)
get().fillExtData(id); get().fillExtData(id);
get().fillExtData(id, true);
get().fillExtCrateData(id); get().fillExtCrateData(id);
get().fillOrderExtData(); get().fillOrderExtData();
get().fillWarnings(id); get().fillWarnings(id);
@ -557,15 +585,18 @@ const useCart = ((set, get) => ({
}, },
addCardFromCatalog: (crate_to, index_from, index_to, just_mounted) => { addCardFromCatalog: (crate_to, index_from, index_to, just_mounted) => {
const dest = crate_to || get().active_crate; const isCrateless = toArray(index_from).some(value => get().getCardDescription(value).crateless === true);
const [isHorizontal, crateTo] = crate_to ? unwrapCrateId(crate_to) : [false, crate_to];
const isHorizontalOnly = toArray(index_from).some(value => !!get().getCardDescription(value).horizontal);
const dest = isCrateless ? "spare" : crateTo || get().active_crate;
if (!dest) { if (!dest) {
console.warn("No destination"); console.warn("No destination");
get().noDestinationWarning(); get().noDestinationWarning();
return {}; return {};
} }
get().showNotification(dest, index_to); get().showNotification(dest, index_to, isHorizontalOnly);
get()._addCardFromCatalog(dest, index_from, index_to) get()._addCardFromCatalog(dest, index_from, index_to, isHorizontalOnly)
get().fillExtData(dest); get().fillExtData(dest, isHorizontalOnly);
get().fillWarnings(dest); get().fillWarnings(dest);
get().setActiveCrate(dest); get().setActiveCrate(dest);
get()._updateTotalOrderPrice(); get()._updateTotalOrderPrice();
@ -575,22 +606,25 @@ const useCart = ((set, get) => ({
}, },
moveCard: (crate_from, index_from, crate_to, index_to) => { moveCard: (crate_from, index_from, crate_to, index_to) => {
get()._moveCard(crate_from, index_from, crate_to, index_to); const [isHorizontal, crateFrom] = unwrapCrateId(crate_from)
get().fillExtData(crate_to); const [_, crateTo] = unwrapCrateId(crate_to)
get().fillWarnings(crate_to); get()._moveCard(crateFrom, index_from, crateTo, index_to, isHorizontal);
get().setActiveCrate(crate_to); get().fillExtData(crateTo, isHorizontal);
get().fillWarnings(crateTo);
get().setActiveCrate(crateTo);
get()._updateTotalOrderPrice(); get()._updateTotalOrderPrice();
if (crate_from !== crate_to) { if (crateFrom !== crate_to) {
get().fillExtData(crate_from); get().fillExtData(crateFrom, isHorizontal);
get().fillWarnings(crate_from); get().fillWarnings(crateFrom);
} }
}, },
deleteCard: (crate_id, index) => { deleteCard: (crate_id, index, horizontal) => {
get()._deleteCard(crate_id, index); const [isHorizontal, crateId] = horizontal ? [horizontal, crate_id] : unwrapCrateId(crate_id);
get().fillExtData(crate_id); get()._deleteCard(crateId, index, isHorizontal);
get().fillWarnings(crate_id); get().fillExtData(crateId, isHorizontal);
get().fillWarnings(crateId);
get()._updateTotalOrderPrice(); get()._updateTotalOrderPrice();
if (crate_id === get().highlighted.crate && index === get().highlighted.card) get().highlightReset() if (crateId === get().highlighted.crate && index === get().highlighted.card) get().highlightReset()
}, },
clearCrate: (id) => { clearCrate: (id) => {
get()._clearCrate(id); get()._clearCrate(id);
@ -598,16 +632,19 @@ const useCart = ((set, get) => ({
get()._updateTotalOrderPrice(); get()._updateTotalOrderPrice();
}, },
updateOptions: (crate_id, index, new_options) => { updateOptions: (crate_id, index, new_options, horizontal) => {
get()._updateOptions(crate_id, index, new_options); get()._updateOptions(crate_id, index, new_options, horizontal);
get().fillExtData(crate_id); get().fillExtData(crate_id, horizontal);
get().fillWarnings(crate_id); if (!horizontal) {
get().fillWarnings(crate_id);
}
}, },
initExtData: () => { initExtData: () => {
get().fillOrderExtData(); get().fillOrderExtData();
get().crates.forEach((crate, _i) => { get().crates.forEach((crate, _i) => {
get().fillExtData(crate.id); get().fillExtData(crate.id, true);
get().fillExtData(crate.id, false);
get().fillExtCrateData(crate.id); get().fillExtCrateData(crate.id);
}) })
get()._updateTotalOrderPrice(); get()._updateTotalOrderPrice();

View File

@ -6,20 +6,23 @@ const shop_data = {
id: 'rack', id: 'rack',
name: 'Rack mountable crate', name: 'Rack mountable crate',
price: 550, price: 550,
hp: 84 hp: 84,
options: "options"
}, },
desktop: { desktop: {
id: 'desktop', id: 'desktop',
name: 'Desktop crate', name: 'Desktop crate',
price: 500, price: 500,
hp: 42 hp: 42,
options: "options"
}, },
no_crate: { no_crate: {
id: 'no_crate', id: 'no_crate',
name: 'Spare cards', name: 'Spare items',
price: 0, price: 0,
hp: -1, hp: -1,
warnings_disabled: true warnings_disabled: true,
options: "crateless_options"
} }
}, },
crateModeOrder: [ crateModeOrder: [
@ -1103,10 +1106,24 @@ const shop_data = {
validator: {name: "ipv4or6"}, validator: {name: "ipv4or6"},
fallback: {text: "DHCP", checked: false}, fallback: {text: "DHCP", checked: false},
tip: "Set up IP address used by the device"}}, tip: "Set up IP address used by the device"}},
{type: "SwitchLine", args: {title: "MQTT broker address", outvar: "broker",
validator: {name: "ipv4OrHost"},
fallback: {text: "mqtt", checked: false},
tip: "Set up domain name or IPv4 of the MQTT broker"}},
{type: "Switch", args: {title: "Ext power", outvar: "ext_pwr", "tip": "Use external power supply in order to reduce number of used EEM connectors"}}, {type: "Switch", args: {title: "Ext power", outvar: "ext_pwr", "tip": "Use external power supply in order to reduce number of used EEM connectors"}},
{type: "Switch", args: {title: "Term #0", outvar: "term_0", tip: "Enable termination on ADC channel #0"}}, {type: "Switch", args: {title: "Term #0", outvar: "term_0", tip: "Enable termination on ADC channel #0"}},
{type: "Switch", args: {title: "Term #1", outvar: "term_1", tip: "Enable termination on ADC channel #1"}} {type: "Switch", args: {title: "Term #1", outvar: "term_1", tip: "Enable termination on ADC channel #1"}}
], ],
crateless_options: [
{type: "SwitchLine", args: {title: "IP", outvar: "ip",
validator: {name: "ipv4or6"},
fallback: {text: "DHCP", checked: false},
tip: "Set up IP address used by the device"}},
{type: "SwitchLine", args: {title: "MQTT broker address", outvar: "broker",
validator: {name: "ipv4OrHost"},
fallback: {text: "mqtt", checked: false},
tip: "Set up domain name or IPv4 of the MQTT broker"}},
],
options_class: "stabilizer", options_class: "stabilizer",
size: 'small', size: 'small',
warnings: [ warnings: [
@ -1137,10 +1154,24 @@ const shop_data = {
validator: {name: "ipv4or6"}, validator: {name: "ipv4or6"},
fallback: {text: "DHCP", checked: false}, fallback: {text: "DHCP", checked: false},
tip: "Set up IP address used by the device"}}, tip: "Set up IP address used by the device"}},
{type: "SwitchLine", args: {title: "MQTT broker address", outvar: "broker",
validator: {name: "ipv4OrHost"},
fallback: {text: "mqtt", checked: false},
tip: "Set up domain name or IPv4 of the MQTT broker"}},
{type: "Switch", args: {title: "Ext power", outvar: "ext_pwr", "tip": "Use external power supply in order to reduce number of used EEM connectors"}}, {type: "Switch", args: {title: "Ext power", outvar: "ext_pwr", "tip": "Use external power supply in order to reduce number of used EEM connectors"}},
{type: "Switch", args: {title: "Term #0", outvar: "term_0", tip: "Enable termination on ADC channel #0"}}, {type: "Switch", args: {title: "Term #0", outvar: "term_0", tip: "Enable termination on ADC channel #0"}},
{type: "Switch", args: {title: "Term #1", outvar: "term_1", tip: "Enable termination on ADC channel #1"}} {type: "Switch", args: {title: "Term #1", outvar: "term_1", tip: "Enable termination on ADC channel #1"}}
], ],
crateless_options: [
{type: "SwitchLine", args: {title: "IP", outvar: "ip",
validator: {name: "ipv4or6"},
fallback: {text: "DHCP", checked: false},
tip: "Set up IP address used by the device"}},
{type: "SwitchLine", args: {title: "MQTT broker address", outvar: "broker",
validator: {name: "ipv4OrHost"},
fallback: {text: "mqtt", checked: false},
tip: "Set up domain name or IPv4 of the MQTT broker"}},
],
options_class: "stabilizer", options_class: "stabilizer",
size: 'small', size: 'small',
warnings: [ warnings: [
@ -1226,7 +1257,25 @@ const shop_data = {
'100Base-T Ethernet with PoE.' '100Base-T Ethernet with PoE.'
], ],
options: [ options: [
{type: "Switch", args: {title: "Ext power", outvar: "ext_pwr", "tip": "Use external power supply in order to reduce number of used EEM connectors"}} {type: "Switch", args: {title: "Ext power", outvar: "ext_pwr", "tip": "Use external power supply in order to reduce number of used EEM connectors"}},
{type: "SwitchLine", args: {title: "Static IPv4", outvar: "ip",
validator: {name: "ipv4"},
fallback: {text: "0.0.0.0", checked: false},
tip: "Set up static IPv4 address used by the device"}},
{type: "SwitchLine", args: {title: "MQTT broker address", outvar: "broker",
validator: {name: "ipv4OrHost"},
fallback: {text: "mqtt", checked: false},
tip: "Set up domain name or IPv4 of the MQTT broker"}},
],
crateless_options: [
{type: "SwitchLine", args: {title: "Static IPv4", outvar: "ip",
validator: {name: "ipv4"},
fallback: {text: "0.0.0.0", checked: false},
tip: "Set up static IPv4 address used by the device"}},
{type: "SwitchLine", args: {title: "MQTT broker address", outvar: "broker",
validator: {name: "ipv4OrHost"},
fallback: {text: "mqtt", checked: false},
tip: "Set up domain name or IPv4 of the MQTT broker"}},
], ],
size: 'small', size: 'small',
warnings: [ warnings: [
@ -1251,6 +1300,12 @@ const shop_data = {
'100Base-T Ethernet with PoE.', '100Base-T Ethernet with PoE.',
'Can stabilize temperature of Sinara 5432 DAC or external devices containing TEC and thermistor.' 'Can stabilize temperature of Sinara 5432 DAC or external devices containing TEC and thermistor.'
], ],
crateless_options: [
{type: "SwitchLine", args: {title: "IPv4", outvar: "ip",
validator: {name: "ipv4"},
fallback: {text: "192.168.1.26/24", checked: false},
tip: "Set up IP address used by the device"}},
],
size: 'small', size: 'small',
consumes: { consumes: {
hp: 4 hp: 4
@ -1302,12 +1357,26 @@ const shop_data = {
validator: {name: "ipv4or6"}, validator: {name: "ipv4or6"},
fallback: {text: "DHCP", checked: false}, fallback: {text: "DHCP", checked: false},
tip: "Set up IP address used by the device"}}, tip: "Set up IP address used by the device"}},
{type: "SwitchLine", args: {title: "MQTT broker address", outvar: "broker",
validator: {name: "ipv4OrHost"},
fallback: {text: "mqtt", checked: false},
tip: "Set up domain name or IPv4 of the MQTT broker"}},
{type: "Switch", args: {title: "Ext power", outvar: "ext_pwr", "tip": "Use external power supply in order to reduce number of used EEM connectors"}}, {type: "Switch", args: {title: "Ext power", outvar: "ext_pwr", "tip": "Use external power supply in order to reduce number of used EEM connectors"}},
{type: "SwitchLine", args: {title: "Ext CLK", outvar: "ext_clk", {type: "SwitchLine", args: {title: "Ext CLK", outvar: "ext_clk",
fallback: {text: "125 MHz", checked: false}, validator: {name: "frequency", params: {min: 10e6, max: 1e9}}}}, fallback: {text: "125 MHz", checked: false}, validator: {name: "frequency", params: {min: 10e6, max: 1e9}}}},
{type: "Switch", args: {title: "Termination #0", outvar: "term_0", tip: "Enable termination on ADC channel #0"}}, {type: "Switch", args: {title: "Termination #0", outvar: "term_0", tip: "Enable termination on ADC channel #0"}},
{type: "Switch", args: {title: "Termination #1", outvar: "term_1", tip: "Enable termination on ADC channel #1"}} {type: "Switch", args: {title: "Termination #1", outvar: "term_1", tip: "Enable termination on ADC channel #1"}}
], ],
crateless_options: [
{type: "SwitchLine", args: {title: "Static IPv4", outvar: "ip",
validator: {name: "ipv4"},
fallback: {text: "0.0.0.0", checked: false},
tip: "Set up static IPv4 address used by the device"}},
{type: "SwitchLine", args: {title: "MQTT broker address", outvar: "broker",
validator: {name: "ipv4OrHost"},
fallback: {text: "mqtt", checked: false},
tip: "Set up domain name or IPv4 of the MQTT broker"}},
],
size: 'big', size: 'big',
warnings: [ warnings: [
"no_eem_source", "no_eem_source",
@ -1374,6 +1443,27 @@ const shop_data = {
hp: 8, hp: 8,
}, },
}, },
'afws': {
id: 'afws',
name: 'Subscription',
name_number: 'AFWS',
name_codename: '',
price: 800,
image: null,
horizontal: true,
specs: [
"Artiq Firmware Service for one variant for one year.",
"Includes support at helpdesk.",
"Included with purchase of any Carrier with no additional cost.",
],
crateless: true,
crateless_options: [
{type: "Line", args: {title: "Variant name", outvar: "variant_name", fallback: "",
tip: "Variant name can be found on the sticker on top of the crate. If you don't have one, leave the preferred name here."}},
],
size: 'big',
warnings: [],
},
}, },
columns: { columns: {
@ -1431,6 +1521,7 @@ const shop_data = {
'koster', 'koster',
'eem_pwr_mod', 'eem_pwr_mod',
'kirdy', 'kirdy',
'afws',
]} ]}
], ],
}, },
@ -1440,14 +1531,16 @@ const shop_data = {
crate_mode: "rack", crate_mode: "rack",
fan_tray: false, fan_tray: false,
items: [], items: [],
h_items: [],
warnings: [], warnings: [],
occupiedHP: 0, occupiedHP: 0,
}, },
{ {
id: "spare", id: "spare",
name: "Spare cards", name: "Spare items",
crate_mode: "no_crate", crate_mode: "no_crate",
items: [], items: [],
h_items: [],
warnings: [], warnings: [],
occupiedHP: 0, occupiedHP: 0,
} }